package edu.ubb.remeye.server.communication.handler;

import java.awt.image.BufferedImage;
import java.util.List;

import org.apache.mina.core.session.IoSession;
import org.apache.mina.statemachine.StateControl;
import org.apache.mina.statemachine.annotation.IoHandlerTransition;
import org.apache.mina.statemachine.annotation.State;
import org.apache.mina.statemachine.context.AbstractStateContext;
import org.codecompany.jeha.ExceptionHandler;
import org.codecompany.jeha.core.HandlerUtil;

import edu.ubb.remeye.communication.MessageType;
import edu.ubb.remeye.communication.message.Message;
import edu.ubb.remeye.control.Control;
import edu.ubb.remeye.encodings.EncodingType;
import edu.ubb.remeye.encodings.encoders.ImageEncoder;
import edu.ubb.remeye.encodings.encoders.bmp.BMPImageEncoder;
import edu.ubb.remeye.encodings.encoders.jpeg.JPEGImageEncoder;
import edu.ubb.remeye.encodings.encoders.png.PNGImageEncoder;
import edu.ubb.remeye.exception.ImageEncodingException;
import edu.ubb.remeye.exception.InaccessibleServiceException;
import edu.ubb.remeye.exception.OSGIBundleInstallationException;
import edu.ubb.remeye.exception.OSGIFrameworkStartException;
import edu.ubb.remeye.exception.OSGIFrameworkStopException;
import edu.ubb.remeye.exception.ScreenCaptureException;
import edu.ubb.remeye.exception.handler.RemEyeExceptionHandler;
import edu.ubb.remeye.gui.event.EventType;
import edu.ubb.remeye.gui.internationalization.LanguageManager;
import edu.ubb.remeye.security.Security;
import edu.ubb.remeye.security.ResultType;
import edu.ubb.remeye.security.SecurityType;
import edu.ubb.remeye.security.manager.none.ServerNoneAuthenticator;
import edu.ubb.remeye.security.manager.vnc.ServerVNCAuthenticator;
import edu.ubb.remeye.server.plugin.PluginManager;
import edu.ubb.remeye.server.plugin.service.EventService;
import edu.ubb.remeye.server.screen.capture.DesktopScreenCapturer;
import edu.ubb.remeye.server.screen.capture.ScreenCapturer;
import edu.ubb.remeye.server.settings.ServerRuntimeSettings;
import edu.ubb.remeye.server.settings.ServerSetter;

import static org.apache.mina.statemachine.event.IoHandlerEvents.*;

/**
 * Class implementing the communication protocol using Apache MINA State Machine and
 * Apache MINA IoHandler events.
 * 
 * @author Andrew Ianicicau
 *
 */
@ExceptionHandler(handler=RemEyeExceptionHandler.class)
public class ServerStateHandler {

	///////////////////////////////////////////////////////////////////////////////////
	////		ATTRIBUTES - ServerStateHandler
	///////////////////////////////////////////////////////////////////////////////////
	
	/**
	 * Screen capturer.
	 */
	private ScreenCapturer screenCapturer;
	
	/**
	 * The event service interface for plug-in.
	 */
	private EventService eventService;
	
	/**
	 * Seconds to wait for session timeout.
	 */
	private static final int TIMEOUT_SECONDS = 60;


	
	///////////////////////////////////////////////////////////////////////////////////
	////		STATES - ServerStateHandler
	///////////////////////////////////////////////////////////////////////////////////
	
	/**
	 * Base state for unhandled events.
	 */
	@State
	public static final String BASE = "BASE";
	
	/**
	 * Connection established.
	 */
	@State(BASE)
	public static final String CONNECTED = "CONNECTED";
	
	/**
	 * Authentication in progress.
	 */
	@State(BASE)
	public static final String AUTHENTICATION = "AUTHENTICATION";
	
	/**
	 * Authentication finished.
	 */
	@State(BASE)
	public static final String AUTHENTICATED = "AUTHENTICATED";
	
	/**
	 * Set sharing settings.
	 */
	@State(BASE)
	public static final String SET = "SET";
	
	/**
	 * Desktop is shared.
	 */
	@State(BASE)
	public static final String SHARED = "SHARED";

	
	
	///////////////////////////////////////////////////////////////////////////////////
	////		CLASSES - ServerStateHandler
	///////////////////////////////////////////////////////////////////////////////////

	/**
	 * Class containing state and related attributes for server.
	 */
	public static class ServerContext extends AbstractStateContext {

		///////////////////////////////////////////////////////////////////////////////////
		////		ATTRIBUTES - ServerContext
		///////////////////////////////////////////////////////////////////////////////////
		
		/**
		 * Client identifier.
		 */
		private long clientId;
		
		/**
		 * Security type in use.
		 */
		private SecurityType security;
		
		/**
		 * Security manager.
		 */
		private Security securityManager;
		
		/**
		 * Shows if client is in control of server.
		 */
		private boolean control;
		
		/**
		 * Image encoder.
		 */
		private ImageEncoder imageEncoder;
		
		
		
		///////////////////////////////////////////////////////////////////////////////////
		////		CONSTRUCTORS - ServerContext
		///////////////////////////////////////////////////////////////////////////////////

		public ServerContext() {
			super();
		}

		
		
		///////////////////////////////////////////////////////////////////////////////////
		////		METHODS - ServerContext
		///////////////////////////////////////////////////////////////////////////////////
		
		public SecurityType getSecurity() {
			return security;
		}
		
		public void setSecurity(SecurityType security) {
			this.security = security;
		}

		public Security getSecurityManager() {
			return securityManager;
		}

		public void setSecurityManager(Security securityManager) {
			this.securityManager = securityManager;
		}

		public long getClientId() {
			return clientId;
		}

		public void setClientId(long clientId) {
			this.clientId = clientId;
		}

		public boolean isControl() {
			return control;
		}

		public void setControl(boolean control) {
			this.control = control;
		}
		
		public ImageEncoder getImageEncoder() {
			return imageEncoder;
		}

		public void setImageEncoder(ImageEncoder imageEncoder) {
			this.imageEncoder = imageEncoder;
		}
		
	}

	
	
	///////////////////////////////////////////////////////////////////////////////////
	////		METHODS - ServerStateHandler
	///////////////////////////////////////////////////////////////////////////////////

	/**
	 * Method for unhandled events.
	 */
	@IoHandlerTransition(in = BASE, weight = 100)
	public void unhandledEvent() {
		
	}
	
	/**
	 * Closes the session on session idle state considered as a timeout.
	 * @param context Server state context.
	 * @param session Actual client session.
	 */
	@IoHandlerTransition(on = SESSION_IDLE, in = BASE, weight = 100)
	public void timeout(ServerContext context, IoSession session) {
		session.close(true);
		Control.showInformation(LanguageManager.getLanguageManager().getText("timeout"));
	}
	
	/**
	 * Called on SESSION_CREATED event in BASE state resulting a BASE state.
	 * Asks the server user if allows actual client's connection. 
	 * @param context Server state context.
	 * @param session Actual client session.
	 */
	@IoHandlerTransition(on = SESSION_CREATED, in = BASE, next = BASE)
	public void sessionCreated(ServerContext context, IoSession session) {
		// setting session timeout
		session.getConfig().setBothIdleTime(TIMEOUT_SECONDS);
		
		// initialize screen capturer
		try {
			screenCapturer = new DesktopScreenCapturer();
			
			// ask server if allows this connection
			boolean allowConnection = Control.getAnswer(LanguageManager.getLanguageManager().getText("allowconnection"));
			if(!allowConnection) {
				session.close(true);
			}
		} catch (ScreenCaptureException e) {
			HandlerUtil.handle(e);
			session.close(true);
		}
	}
	
	/**
	 * Called on SESSION_OPENED event in BASE state resulting a CONNECTED state.
	 * Sends the client id and security list.
	 * @param context Server state context.
	 * @param session Actual client session.
	 */
	@IoHandlerTransition(on = SESSION_OPENED, in = BASE, next = CONNECTED)
	public void sessionOpened(ServerContext context, IoSession session) {
		// send id message
		context.setClientId(session.getId());
		Message idMessage = createIdMessage(context.getClientId());
		session.write(idMessage);
		
		// send available security list
		Message securityListMessage = createSecurityListMessage();
		session.write(securityListMessage);
	}
	
	/**
	 * Called on MESSAGE_RECEIVED event in CONNECTED state resulting a AUTHENTICATION state.
	 * Sets security type to use selected one by client and starts authentication if selected
	 * type is valid.
	 * @param context Server state context.
	 * @param session Actual client session.
	 * @param message Message received.
	 */
	@IoHandlerTransition(on = MESSAGE_RECEIVED, in = CONNECTED, next = AUTHENTICATION)
	public void setSecurity(ServerContext context, IoSession session, Message message) {
		// check for valid message, must be SECURITY_ANSWER
		if(message.getMessageType().equals(MessageType.SECURITY_ANSWER)) {
			// check for valid selected security type
			if(gotoAuthentication(message, context)) {
				startAuthentication(context, session);
				
				// setting session idle time to 1
				session.getConfig().setBothIdleTime(1);
			} else {
				session.close(true);
			}
		} else {
			StateControl.breakAndGotoNext(CONNECTED);
		}
	}
	
	/**
	 * Called on MESSAGE_RECEIVED event in AUTHENTICATION state resulting the same state.
	 * Forwards the received messages to security manager thread.
	 * @param context Server state context.
	 * @param session Actual client session.
	 * @param message Message received.
	 */
	@IoHandlerTransition(on = MESSAGE_RECEIVED, in = AUTHENTICATION, next = AUTHENTICATION)
	public void authenticationMessage(ServerContext context, IoSession session, Message message) {
		// check for valid message, must be AUTHENTICATION
		if(message.getMessageType().equals(MessageType.AUTHENTICATION)) {
			context.getSecurityManager().forwardMessage(message);
		}
	}
	
	/**
	 * Called on SESSION_IDLE event in AUTHENTICATION state resulting AUTHENTICATED state.
	 * When authentication finished sends a result message to client according to 
	 * authentication state.
	 * @param context Server state context.
	 * @param session Actual client session.
	 */
	@IoHandlerTransition(on = SESSION_IDLE, in = AUTHENTICATION, next = AUTHENTICATED)
	public void authenticated(ServerContext context, IoSession session) {
		// checks for finished authentication
		if(authenticationFinished(session)) {
			// setting session idle time to back to timeout
			session.getConfig().setBothIdleTime(TIMEOUT_SECONDS);
			
			// checks for authentication result
			if(getAuthenticationResult(session)) {
				// send succeeded message
				Message resultMessage = createResultMessage(ResultType.SUCCEEDED);
				session.write(resultMessage);
			} else {
				// send failed message
				Message resultMessage = createResultMessage(ResultType.FAILED);
				session.write(resultMessage);
				session.close(false);
			}
		}
	}
	
	/**
	 * Called on MESSAGE_RECEIVED event in AUTHENTICATED state resulting SET state.
	 * Receives an encoding message containing the desired encoding, selects 
	 * the convenient encoding and returns an answer message on the selected encoding.
	 * @param context Server state context.
	 * @param session Actual client session.
	 * @param message Message received.
	 */
	@IoHandlerTransition(on = MESSAGE_RECEIVED, in = AUTHENTICATED, next = SET)
	public void setEncoding(ServerContext context, IoSession session, Message message) {
		// check for valid message, must be ENCODING
		if(message.getMessageType().equals(MessageType.ENCODING)) {
			// select encoding
			EncodingType selected = selectEncoding(context, message);
			
			// send encoding answer message
			Message encodingAnswer = createEncodingAnswerMessage(selected);
			session.write(encodingAnswer);
		} else {
			StateControl.breakAndGotoNext(AUTHENTICATED);
		}
	}
	
	/**
	 * Called on MESSAGE_RECEIVED event in SET state resulting SHARED state.
	 * Receives a control request message, makes a decision on it and returns the answer.
	 * If control is allowed initializes the control plug-in.
	 * @param context Server state context.
	 * @param session Actual client session.
	 * @param message Message received.
	 */
	@IoHandlerTransition(on = MESSAGE_RECEIVED, in = SET, next = SHARED)
	public void controlRequest(ServerContext context, IoSession session, Message message) {
		// check for valid message, must be CONTROL_REQUEST
		if(message.getMessageType().equals(MessageType.CONTROL_REQUEST)) {
			boolean controlRequest = getControlRequest(message);
			
			// if request is true, ask server if allows client to control the server
			boolean serverAllow = false;
			if(controlRequest) {
				serverAllow = Control.getAnswer(LanguageManager.getLanguageManager().getText("allowcontrol"));
			}
			
			boolean getControl = evaluateControlRequest(controlRequest, serverAllow);

			// set control
			setControl(context, getControl);	
			
			// send control answer message
			Message controlMessage = createControlMessage(context);
			session.write(controlMessage);
			
			if(context.isControl()) {
				try {
					PluginManager.getInstance().startManager();
				} catch (OSGIFrameworkStartException e) {
					HandlerUtil.handle(e);
				} catch (OSGIBundleInstallationException e) {
					HandlerUtil.handle(e);
				}
				
				try {
					eventService = PluginManager.getInstance().getEventService();
				} catch (InaccessibleServiceException e) {
					HandlerUtil.handle(e, context);
					context.setControl(false);
				}

			}
			
		} else {
			StateControl.breakAndGotoNext(SET);
		}
	}
	
	/**
	 * Called on MESSAGE_RECEIVED event in SHARED state resulting SHARED state.
	 * When receives an update request, makes a screen capture and sends it to 
	 * client in a data message.
	 * @param context Server state context.
	 * @param session Actual client session.
	 * @param message Message received.
	 */
	@IoHandlerTransition(on = MESSAGE_RECEIVED, in = SHARED, next = SHARED)
	public void receiveEvent(ServerContext context, IoSession session, Message message) {
		// check for valid message, must be UPDATE
		if(message.getMessageType().equals(MessageType.UPDATE)) {		
			BufferedImage image = screenCapturer.capture();
			
			try {
				byte[] imageBytes = context.getImageEncoder().encode(image);
				
				Message imageMessage = new Message(MessageType.DATA, imageBytes);
				session.write(imageMessage);
			} catch(ImageEncodingException e) {
				HandlerUtil.handle(e);
				session.close(true);
			}
			
		}
		
		// check for valid message, must be EVENT
		if(message.getMessageType().equals(MessageType.EVENT)) {
			if(context.isControl()) {
				byte[] data = message.getMessage();
				int ordinal = getIntegerFromBytes(data, 0);
				EventType eventType = EventType.getEventType(ordinal);
				doEvent(eventType, data);
			}
		}
	}
	
	/**
	 * Called on SESSION_CLOSED event in BASE state.
	 * If the actual client is in control it removes and stops the plug-in manager.
	 * @param context Server state context.
	 * @param session Actual client session.
	 */
	@IoHandlerTransition(on = SESSION_CLOSED, in = BASE)
	public void sessionClosed(ServerContext context, IoSession session) {
		if(ServerRuntimeSettings.getinstance().getControlClient() == context.getClientId())
			ServerRuntimeSettings.getinstance().setControlClient(0);
		
		try {
			PluginManager.getInstance().stopManager();
		} catch (OSGIFrameworkStopException e) {
			HandlerUtil.handle(e);
		}
	}
	
	/**
	 * Returns a message of type IDENTIFIER containing the identifier number.
	 * @param identifier Identifier number.
	 * @return IDENTIFIER message.
	 */
	private Message createIdMessage(long identifier) {
		byte[] id = new byte[1];
		id[0] = (byte) identifier;
		Message idMessage = new Message(MessageType.IDENTIFIER, id);
		return idMessage;
	}
	
	/**
	 * Returns a message of type SECURITY_LIST containing security types list.
	 * supported by server.
	 * @return SECURITY_LIST message.
	 */
	private Message createSecurityListMessage() {
		List<SecurityType> list = ServerSetter.getInstance().getSecurityTypes();
		byte[] securityList = new byte[list.size()];
		for(int i = 0; i < securityList.length; i++) {
			securityList[i] = (byte) list.get(i).ordinal();
		}
		
		Message securityListMessage = new Message(MessageType.SECURITY_LIST, securityList);
		return securityListMessage;
	}
	
	/**
	 * Returns a boolean value, according to security type in message is INVALID or an other one 
	 * and sets the security type.
	 * @param message SECURITY_ANSWER message.
	 * @param context Server state context.
	 * @return True if security type is other than INVALID, else false.
	 */
	private boolean gotoAuthentication(Message message, ServerContext context) {
		SecurityType selected = SecurityType.getSecurityType(message.getMessage()[0]);
		if(selected.equals(SecurityType.INVALID))
			return false;
		
		// setting security type
		context.setSecurity(selected);
		return true;
	}
	
	/**
	 * Returns the server security manager to use, according to security type.
	 * @param type Security type to use.
	 * @param session Actual client session.
	 * @return Security manager.
	 */
	private Security getSecurityManager(SecurityType type, IoSession session) {
		switch(type) {
		case NONE:
			return new ServerNoneAuthenticator(session);
		case VNC:
			return new ServerVNCAuthenticator(session);
		default:
			return null;
		}
	}
	
	/**
	 * Starts the authentication thread using the right security manager.
	 * @param context Server state context.
	 * @param session Actual client session.
	 */
	private void startAuthentication(ServerContext context, IoSession session) {
		// set security manager
		Security manager = getSecurityManager(context.getSecurity(), session);
		context.setSecurityManager(manager);
		
		// start authentication thread
		Thread securityThread = new Thread(context.getSecurityManager());
		securityThread.start();
	}
	
	/**
	 * Returns the state of authentication.
	 * @param session Actual client session.
	 * @return True if authentication finished, else false.
	 */
	private boolean authenticationFinished(IoSession session) {
		if(session.getAttribute("client_authenticated") != null)
			return true;
		return false;
	}
	
	/**
	 * Returns the result of authentication.
	 * @param session Actual client session.
	 * @return True if authentication succeeded, else false.
	 */
	private boolean getAuthenticationResult(IoSession session) {
		if(session.getAttribute("result_ok") != null)
			return true;
		return false;
	}
	
	/**
	 * Returns the encoding selected from the server's list according to the
	 * client's desired encoding in the message.
	 * @param context Server state context.
	 * @param message ENCODING message.
	 * @return Selected encoding type.
	 */
	private EncodingType selectEncoding(ServerContext context, Message message) {
		List<EncodingType> list = ServerSetter.getInstance().getEncodingTypes();
		int ordinal = message.getMessage()[0];
		EncodingType desired = EncodingType.getEncodingType(ordinal);
		EncodingType selected = EncodingType.listContains(list, desired);
		context.setImageEncoder(getImageEncoder(selected));
		return selected;
	}
	
	/**
	 * Returns a message of type ENCODING_ANSWER containing the encoding selection result.
	 * @param type Encoding type to put in message. 
	 * @return ENCODING_ANSWER message.
	 */
	private Message createEncodingAnswerMessage(EncodingType type) {
		byte[] encoding = new byte[1];
		encoding[0] = (byte) type.ordinal();
		Message encodingMessage = new Message(MessageType.ENCODING_ANSWER, encoding);
		return encodingMessage;
	}
	
	/**
	 * Returns a message of type AUTHENTICATION_RESULT containing the authentication result byte.
	 * @param result Type of result.
	 * @return AUTHENTICATION_RESULT message.
	 */
	private Message createResultMessage(ResultType result) {
		byte[] res = new byte[1];
		res[0] = (byte) result.ordinal();
		Message resultMessage = new Message(MessageType.AUTHENTICATION_RESULT, res);
		return resultMessage;
	}
	
	private boolean evaluateControlRequest(boolean request, boolean allowControl) {
		return request && allowControl;
	}
	
	private boolean getControlRequest(Message message) {
		boolean control = false;
		if(message.getMessage()[0] > 0)
			control = true;
		return control;
	}
	
	/**
	 * Sets the control request result in context.
	 * @param context Server state context.
	 * @param control Control boolean value.
	 */
	private void setControl(ServerContext context, boolean control) {
		context.setControl(control);
		if(control)
			ServerRuntimeSettings.getinstance().setControlClient(context.getClientId());
	}
	
	/**
	 * Returns a control answer message.
	 * @param context Server state context.
	 * @return CONTROL_ANSWER message.
	 */
	private Message createControlMessage(ServerContext context) {
		byte[] control = new byte[1];
		if(context.isControl())
			control[0] = Byte.MAX_VALUE;
		Message controlMessage = new Message(MessageType.CONTROL_ANSWER, control);
		return controlMessage;
	}
	
	/**
	 * Returns an integer (4 byte) extracted from a byte array starting from a given index.
	 * @param array Byte array containing the integer.
	 * @param index Start byte.
	 * @return Extracted integer.
	 */
	private int getIntegerFromBytes(byte[] array, int index) {
		int intNumber = (array[index] << 24)
		+ ((array[index+1] & 0xFF) << 16)
		+ ((array[index+2] & 0xFF) << 8)
		+ (array[index+3] & 0xFF);		
		return intNumber;
	}
	
	/**
	 * Calls the event service methods on events.
	 * @param event Type of event.
	 * @param data Event data.
	 */
	private void doEvent(EventType event, byte[] data) {
		if(eventService != null) {
			switch(event) {
			case KEY_PRESSED:
				int pressKeyCode = getIntegerFromBytes(data, 4);
				eventService.keyPress(pressKeyCode);
				break;
			case KEY_RELEASED:
				int releaseKeyCode = getIntegerFromBytes(data, 4);
				eventService.keyRelease(releaseKeyCode);
				break;
			case MOUSE_MOVED:
				int x = getIntegerFromBytes(data, 4);
				int y = getIntegerFromBytes(data, 8);
				eventService.mouseMove(x, y);
				break;
			case MOUSE_CLICKED:
				int clickButton = getIntegerFromBytes(data, 4);
				int clickX = getIntegerFromBytes(data, 8);
				int clickY = getIntegerFromBytes(data, 12);
				eventService.mouseClick(clickButton, clickX, clickY);
				break;
			case MOUSE_DOUBLE_CLICKED:
				int doubleClickButton = getIntegerFromBytes(data, 4);
				int doubleClickX = getIntegerFromBytes(data, 8);
				int doubleClickY = getIntegerFromBytes(data, 12);
				eventService.mouseDoubleClick(doubleClickButton, doubleClickX, doubleClickY);
				break;
			case MOUSE_PRESSED:
				int pressButton = getIntegerFromBytes(data, 4);
				int pressX = getIntegerFromBytes(data, 8);
				int pressY = getIntegerFromBytes(data, 12);
				eventService.mousePressed(pressButton, pressX, pressY);
				break;
			case MOUSE_RELEASED:
				int releaseButton = getIntegerFromBytes(data, 4);
				int releaseX = getIntegerFromBytes(data, 8);
				int releaseY = getIntegerFromBytes(data, 12);
				eventService.mouseReleased(releaseButton, releaseX, releaseY);
				break;
			case MOUSE_DRAGGED:
				eventService.mouseDragged();
				break;
			case MOUSE_WHEEL:
				int rotation = getIntegerFromBytes(data, 4);
				eventService.mouseWheel(rotation);
				break;
			}
		}
	}
	
	/**
	 * Return the image encoder.
	 * @param encoding The selected encoder type.
	 * @return Image encoder or null.
	 */
	private ImageEncoder getImageEncoder(EncodingType encoding) {
		switch(encoding) {
		case BMP:
			return new BMPImageEncoder();
		case JPEG:
			return new JPEGImageEncoder();
		case PNG:
			return new PNGImageEncoder();
		default:
			return null;
		}
	}

}
